結論先說:好的 alert 指向使用者影響、可由明確 owner 處理,並附 runbook;無法決定下一步的訊號適合 dashboard 或 ticket,不適合 pager。這篇先講判斷原則、幾個真實事故的教訓,以及怎麼把它們寫成 alert contract 與 PromQL rule;routing、runbook 與審查機制留到下篇。
Day 24 把 dashboard 排成六層,讓 on-call 打開畫面就知道先看哪裡。今天要接上的是下一步:同一組訊號裡,哪些值得把人從睡夢中叫醒,哪些只該留在畫面上等人主動去看。
收到 alert 時,on-call 應能在幾分鐘內答出三個問題:哪個 SLO 或使用者承諾受威脅?誰負責?先做哪個安全動作?缺少任何一項,這則 alert 就無法直接支援處置,只會增加值班負擔。
Etsy 的工程團隊很早就發現這個問題不能靠「調敏感度」解決。他們做的是 Opsweekly:每一則進來的 alert,值班工程師都要標記它是否 actionable(有真的問題、需要人介入處理),再彙整成每週報表,追蹤 actionable 與 non-actionable 的比例是不是持續改善。這個做法把「這條 alert 是不是廢話」從值班工程師的主觀抱怨,變成一條可以每週檢視、可以要求下降的指標。
實務數據印證了 Etsy 當年的直覺有多正確。PagerDuty 2025 年的 State of Digital Operations report 指出,平均 on-call 工程師每週收到約 50 則警報,其中只有 2–5% 真正需要人工介入;Catchpoint 2024 年的調查也顯示,70% 的 SRE 團隊把 alert fatigue 列為前三大營運關切。多數警報沒人真的去看,也沒有人被要求持續回報「這則警報有沒有用」,這才是系統設計問題,不是某個團隊比較粗心。
它說明了一件事:alert 疲勞不是工具太敏感的問題,而是沒有人被要求持續回答「這則 alert 有沒有用」。
這裡有一個常被混在一起講的地方:把「監控涵蓋範圍夠不夠廣」跟「alert 該不該響」當成同一件事。監控範圍廣是好事,愈多維度被量測,事後排查愈有材料可查;但每多接上一條 metric,不代表就該多一條會叫醒人的 pager rule。兩者的差別,可以用一張簡單的分流圖表示:
訊號來源(metrics / logs / traces)
|
+-- 值得長期保留、供排查與趨勢分析 --> dashboard、log、trace store
|
+-- 值得追蹤但不急 --> review queue、週報
|
+-- 正在傷害使用者、且有人能做點什麼 --> pager
三條路徑的原始資料可以完全相同,差別只在於「這個訊號現在需要什麼回應節奏」。把所有訊號都往第三條路塞,等於把監控系統的廣度直接複製成 pager 的雜訊量——這正是多數團隊 alert 疲勞的起點:不是訊號太多,是分流機制根本不存在,所有東西都被同一種急迫程度對待。這條分流邏輯,會在第九節被展開成一張完整的決策樹。
經典 SRE 的做法,是把「症狀」與「原因」分開對待:availability 與 latency 可以用 error-budget burn alert 化,因為它們直接對應使用者能不能拿到服務、拿到得夠不夠快;dependency error 則要等到確實威脅使用者 SLO 時才 alert,而不是任何一次呼叫失敗都觸發。單次 error、CPU 稍高、單筆品質不佳,這些通常先留給 dashboard 或 review queue,不需要立即叫醒任何人。
## /ask fast burn
1. 確認 SLI window 與 deploy change。
2. 由 dashboard 找 route/outcome,再開相關 trace。
3. 若 upstream timeout,套用既定 degradation;不要無限 retry。
4. 記錄 incident owner 與使用者影響。
runbook 要測過才可信。未測時就該標成草案,不能假稱事故流程已演練過。
這條原則常被誤解成「原因類告警比較不重要,可以晚點再做」。真正的理由不是重要性排序,是可靠度排序:cause alert 的前提是你已經正確窮舉了所有可能傷害使用者的原因,而分散式系統的失敗模式本質上是開放集合——今天加了 LLM provider、明天加了向量資料庫,永遠可能冒出一個你沒預先想到、但確實正在拖垮使用者體驗的新原因。symptom alert 不需要窮舉原因,它只問一句話:使用者拿到的結果,有沒有符合承諾?
Google SRE Workbook 把告警粗分成三類:pages(現在需要人)、tickets(該修但不急)、logging(純記錄)。多數團隊踩的坑,是把這三類混在同一個 severity 欄位裡決定要不要響,而不是先決定「這是哪一種訊號」,再決定「用哪個管道通知」。這個分類方式會在第九節被展開成完整的分流邏輯。
一個常見的誤解是「原因類指標反應比較快,所以比較安全」。這句話只對了一半:原因類指標確實常常比使用者症狀更早出現異常,但「更早出現異常」不等於「更早出現的異常值得叫醒人」。CPU 使用率上升可能是正常的流量高峰,也可能是即將發生的故障前兆——單看這一個數字,你分不出來。真正能分辨的,是它有沒有跟使用者可觀察到的結果一起變化。這也是接下來這個真實案例想說明的:有時候問題不是「原因指標反應太慢」,而是連症狀本身的量測方法都是瞎的。
Honeycomb 工程團隊公開分享過促使他們認真投入 SLO 的關鍵事件:一次只影響約 1–2% 請求的「brown-out」(部分退化,不是整個服務掛掉)。他們原本的健康檢查邏輯要求「連續兩次探測失敗」才觸發告警,但這次事件的失敗是間歇性、分散在整個請求母體裡的,從未連續發生兩次——傳統的二元式(up / down)健康檢查因此完全沒有反應。與此同時,團隊在文章裡的原話是:「our SLO immediately started burning and we realized, relatively quickly, that our users were being affected」。以 good event/total event 比例為基礎的 SLO burn,幾乎即時反映了使用者正在受影響,而不需要等「連續兩次」這種人為設下的門檻剛好被踩到。
這個案例補的是一塊容易被忽略的拼圖:alert 設計不好,不只發生在「訊號存在但沒被當成 alert」(下一節會談到的 Knight Capital),也可能發生在「量測方法本身結構性地看不到某一類故障」。健康檢查探測的是二元狀態,SLI 量的是連續比例——這兩者不是同一件事的兩種寫法,而是解析度完全不同的兩把尺。分散、間歇、只影響一小部分流量的退化,二元尺量不出來。
AI workflow 多了 LLM provider、向量資料庫、GPU 這類新依賴,但判斷要不要 paging 的原則沒變:先看它有沒有威脅到使用者 SLO。provider 逾時率飆高、GPU 佇列塞爆導致大面積回應變慢,這類訊號和傳統 dependency error 一樣,可以走同一套 burn-rate alert。真正棘手的是另一批 AI 特有的訊號(例如單筆 hallucination、單一使用者不喜歡的語氣),它們通常沒有明確的使用者受影響範圍,也答不出「凌晨兩點該做什麼」,這類訊號要走哪條路、由誰決定升級,留到 Day 26 再細談。今天先記住一條分界線:AI 系統多的是新的訊號來源,不是新的 paging 理由。
用一個對照可以讓這條分界線更具體:
新依賴出現異常
+
症狀 alert 的判斷原則沒變(是否威脅使用者 SLO)
=
新的依賴,舊的 paging 邏輯
對比常見的誤解:
新依賴出現異常
+
「AI 系統特殊,所以要有一套全新的告警哲學」
=
每個新技術元件各自一套 severity 定義,
routing 表越來越長,卻沒人講得出哪條規則對應哪個使用者承諾
這個誤解之所以常見,是因為 AI workflow 確實引入了傳統系統少見的新故障向度——模型輸出的品質、prompt 與 retrieval index 的版本、guardrail 是否被繞過。但「這是新故障向度」跟「這需要新的 paging 決策原則」是兩件事。GPU 佇列塞爆本質上就是資源飽和(saturation),跟資料庫連線池被打爆是同一種問題形狀,只是換了個資源種類;provider timeout 本質上就是 dependency 逾時,跟呼叫任何外部 API 逾時是同一種問題形狀。這些訊號完全可以套用第十節會示範的 burn-rate alert 公式,不需要為了「這是 AI」而發明新規則。真正需要新判斷的,是那些沒有清楚使用者受影響範圍、也答不出「on-call 現在能做什麼」的訊號——這批訊號的處理方式,才是 Day 26 要單獨拉出來談的原因。
替 availability、latency、dependency 各寫一條 alert 草案。每條都填 SLI、window、owner、runbook、抑制條件與解除條件。再問一次:「凌晨收到它,我有權做什麼?」答不出來就降級為 dashboard。
這一節的驗證專案(Day25/DIY)刻意沒有直接寫 Prometheus rule YAML,而是先用 Python dataclass 把「一條 alert 該長什麼樣子」定義成一個可以被程式檢查的資料結構,再寫一個 validator 逐條檢查。這個決定背後的理由,其實就是本文一路在強調的東西:如果「這條 alert 夠不夠格 paging」只能靠人眼看過去覺得合理,那審查標準會隨審查者的心情、經驗、當天精神狀況漂移。把審查標準寫成程式碼,等於把第八節那張 contract 表格裡「必填欄位」的判斷,變成一件可以重複執行、不會遺漏、也能在 CI 裡跑的事——這正是它要驗證的第一個論點:alert 的可行動性可以、也應該被結構化地檢查,不是靠審查者的直覺。
AlertRule(dataclass)
├── name / user_impact → 對應第八節 contract 的「User impact」欄
├── owner → 對應「Owner」欄
├── sli / slo → 對應「Primary SLI」「Objective」欄
└── runbook: Runbook
├── steps[0] → 對應「First safe action」欄
└── is_tested: bool → 對應「未測過的 runbook 要如實標示」這條原則
validator.py 做的事,其實就是把「凌晨收到它,我有權做什麼?」這句話拆成四個可以程式化檢查的子問題:這條 alert 有沒有連到使用者影響或 error budget?有沒有明確的 owner?有沒有可執行的第一步?會不會因為單一事件(例如一次 dependency 失敗、一筆 hallucination)就誤觸發?這四個子問題,逐字對應本節開頭的驗收清單。
uv run python scripts/validate_alerts.py 這個指令的預期輸出,是三條規則(Availability / Latency / Dependency)逐一列出檢查結果,每一項檢查(連到使用者影響、有 owner、有第一步、非單一事件觸發、runbook 標示是否正確)各自打勾,最後彙總成「3 alerts passed, 0 failed」。如果讀者照著文章描述的四個問題自己動手改壞其中一條規則——例如把 owner 欄位清空,或是把 latency alert 改成看單一請求而非 P95 聚合值——重新執行後應該會看到對應的檢查項目變成失敗,並且驗證器會清楚指出是哪一項條件不成立,而不是籠統地說「這條規則有問題」。這個「刻意改壞再重跑」的動作,才是這個 DIY 真正想讓讀者體驗的東西:審查標準不是抽象原則,是可以被具體違反、也可以被具體抓出來的規則。
scripts/show_alerts.py 則是給人看的版本,把每條規則展開成人類可讀的八個區段(描述、SLI/SLO、owner、觸發條件、runbook、抑制條件、解除條件、可行動性檢查),模擬的是第十六節 SRE Lab 步驟九「請非作者做 runbook walkthrough」的前置動作——在真的找同事做桌上推演之前,先確認這份文件本身讀起來夠完整,不用另外開三個分頁去湊資訊。
必須誠實說清楚這個 DIY 的邊界。它驗證的是「一條 alert 規則的文件與結構是否符合可行動性的四個必要條件」,這是本文第八節反覆強調的 contract 概念能不能被落實成可檢查的東西。它沒有驗證的東西,至少有三項:規則的 PromQL expression 語法是否正確(那是第十六節提到的 promtool test rules 該做的事);規則接上真實 Prometheus 之後,門檻值訂得合不合理(那需要真實流量或至少合成時間序列去推演,對應第十三節「用規則測試描述你期待的行為」);以及 runbook 裡寫的步驟,交給一個沒參與設計的人,是否真的能在壓力下照著走完(那是「非作者 walkthrough」該做的事,本 DIY 只用 is_tested=False 這個欄位誠實標示「這一步還沒發生」)。三層驗證缺一不可,而這個 DIY 只做了第一層——先確認文件本身完整,是後兩層驗證能開始的前提,不是後兩層驗證的替代品。
寫這個 DIY 的過程中,有兩個具體的坑值得記下來,讀者如果照這個結構自己重建,大概率也會遇到。第一個是 Python dataclass 的欄位順序限制:AlertRule 一開始把 runbook 這類必填欄位放在有預設值的 optional 欄位(例如抑制條件)後面,直接觸發 TypeError: non-default argument follows default argument——這是 dataclass 的語言限制,必填欄位一定要排在有預設值的欄位之前,跟欄位的邏輯重要性無關,純粹是 Python 的語法規則。第二個坑更貼近本文主題:一開始 Runbook 這個資料結構沒有 is_tested 欄位,等於預設每條 runbook 都「看起來完整」,但完整不等於驗證過。補上這個欄位、把預設值訂為 False,才讓驗證器能真的去檢查「有沒有如實標示未測試狀態」,而不是只檢查「有沒有寫 runbook」——這個修正本身,就是第十二節「runbook 要承認未知」這條原則在程式碼層級的具體體現:資料結構如果沒有「我還不知道」這個選項,就會被迫用「已經測過」去填補空白。
本文未設定 alert、發送 page 或驗證 runbook。
判斷一份 alert/runbook 系統好不好,不能只看「有沒有響」。响了之後,還有三段路要走完:訊號有沒有被設計成人看得懂、看懂之後多久能鎖定方向、鎖定方向之後執行的動作本身可不可靠。三個真實事故各自對應其中一段路的斷裂,第四個案例則是反過來,示範這三段路都補上之後恢復能有多快。
2012 年 8 月 1 日,美國做市商 Knight Capital 部署一套交易系統更新,目的是重用一段已停用 8 年的舊功能所使用的旗標欄位。工程師手動把新程式碼部署到 8 台伺服器,過程沒有第二人覆核,也沒有書面程序要求覆核;其中 1 台伺服器漏裝新版,讓已停用的舊邏輯繼續運作——這段舊邏輯不會檢查母單是否已經成交完畢,只會不斷送出子單,形成無窮迴圈下單。市場開盤後到工程團隊真正意識到問題之間,系統其實送出了 97 封提及這個模組、內容是「功能已停用」的 email。SEC 事後的行政處分文件寫得很直白:這些訊息「並非以系統警報的形式設計,因此沒有人在第一時間去看」。等工程團隊終於發現異常,因為沒有 kill switch、也沒有寫好的應變程序,只能在即時交易、每分鐘處理 800 萬股的情況下臨場排查——過程中甚至一度把已經部署正確的伺服器上的新程式碼移除,讓壞掉的邏輯反而跑到更多節點上,越修越糟。整起事故歷時約 45 分鐘,Knight Capital 虧損約 4.6 億美元,超過公司當時的現金儲備,最終被迫尋求緊急收購。
97 封 email 不是沒有訊號,是訊號沒有被設計成 alert——沒有明確的 severity、沒有 owner、沒有「現在該做什麼」。這正是第八節「alert contract」要解決的問題:contract 缺席時,訊號存在也等於沒發生過。
2021 年 6 月 8 日,Fastly CDN 因一項合法客戶組態觸發潛藏軟體缺陷,導致全球多個網站(包含 Reddit、GitHub、亞馬遜等)同時出現錯誤。Fastly 官方事後報告給出的時間軸是:09:47 UTC 開始出現全域異常,09:48 UTC 監控系統就偵測到(一分鐘內),但一直到 10:27 UTC,工程團隊才鎖定是哪一項客戶組態觸發了這個缺陷,10:36 UTC 才開始恢復,11:00 UTC 多數服務已恢復正常,直到 12:35 UTC 才完全緩解。
偵測只花了一分鐘,真正吃掉時間的是中間那近 40 分鐘的「找出是哪個組態」。這正是一份好 runbook 該預先縮短的段落:先把已知的排查路徑、可疑組態的檢查順序寫清楚,而不是仰賴工程師在事故現場從頭推理。Fastly 官方文章對後續改善只表態會做完整事後檢討、評估如何縮短修復時間,並未公開更細的具體行動項目,這裡不宜替它腦補出一套沒有出處的流程。
2017 年 2 月 28 日,一名 AWS S3 團隊成員在執行例行維運指令時,因輸入錯誤移除了遠超預期數量的伺服器容量,意外打掉了兩個關鍵子系統:管理物件中繼資料的 index 子系統,以及負責儲存配置的 placement 子系統。這次真正拖慢復原速度的,不是找根因——AWS 官方事故摘要寫得很清楚,慢的是「重新啟動這些服務,並執行驗證 metadata 完整性所需的安全檢查」,因為這兩個子系統隨著 S3 規模成長,已經好幾年沒有經歷過完整重啟。復原程序寫在那裡,卻從未在這個規模下被真正演練過,第一次跑就是在生產事故現場。事故從 09:37 PST 開始,直到 13:54 PST 才完全恢復,總計約 4 小時 17 分鐘。
前三個案例都是壞掉的示範,容易讓人誤以為「alert 系統很難做對」。值得放一個對照組,看看把前面三段路都補上之後,事故能被壓縮成什麼樣子。
2019 年 7 月 2 日,Cloudflare 全球網路發生一次因單條 WAF 規則觸發的中斷。導火線是一段用來偵測 XSS 攻擊的正規表達式,寫法上會在特定輸入下觸發「catastrophic backtracking」——執行時間隨輸入長度呈指數成長。這條規則部署到全球後,負責處理 HTTP/HTTPS 流量的 CPU 在所有伺服器上幾乎同時衝到接近 100%。Cloudflare 官方事後報告給出精確到分鐘的時間軸:13:31 工程師合併這次變更,13:37 CI 測試通過,13:42 開始全域部署,13:45(部署後僅 3 分鐘)PagerDuty 就因為 WAF 功能異常自動觸發告警。團隊花了 15 分鐘確認 WAF 規則是致因,14:02 提議動用「全域終止 WAF」這個緊急開關,14:07 正式執行,僅僅兩分鐘後(14:09)流量與 CPU 就恢復正常。完全復原(修好正規表達式、重新全域啟用 WAF)落在 14:52,總計約 1 小時 10 分鐘;但真正意義上的「中斷」,從 13:42 部署開始算到 14:09 流量恢復,只有 27 分鐘。
拿這個時間軸跟 Knight Capital 對照,反差非常明顯。兩者都是「一次部署讓潛藏在系統裡的邏輯在生產環境爆炸」,但 Cloudflare 的關鍵差異是:那個「全域終止 WAF」的開關,是事先就存在、且已經被授權可以在緊急狀況下由 on-call 工程師直接按下去的動作。從收到 page(13:45)到提出要用這個開關(14:02),中間的 17 分鐘幾乎全部花在確認根因,而不是花在「我們有沒有權限這樣做」「要不要先問過誰」這類決策摩擦上。Knight Capital 因為完全沒有這種預先核准的緊急動作,工程團隊只能在即時交易現場臨場拼湊應變方式,結果反而讓問題擴大。
這個對照直接呼應第八節 contract 表格裡「First safe action」那一欄為什麼要求「已核准」三個字:一個動作如果要等到事故現場才討論它安不安全、誰有權執行,那麼不管你的偵測速度有多快,中間的決策與授權摩擦都會把恢復時間拖長。Cloudflare 事後公開的兩項改善承諾,也值得注意——把規則部署改成分階段(staged rollout)逐步推出,以及把正規表達式引擎換成有執行時間保證的 re2 或 Rust regex engine。前者是流程層面的護欄,後者是從根本上排除同類故障再次發生的可能,兩者合起來說明一件事:好的事故回應不是終點,事後還要往前補,讓同一種訊號下次連觸發都不會觸發。
這四個案例合起來畫出一份 alert/runbook 系統可能斷裂(或者,如果做對了,可能被壓縮到多短)的完整地圖:Knight Capital 斷在「訊號沒被設計成 alert」,Fastly 斷在「鎖定方向的路徑沒被預先寫好」,AWS S3 斷在「寫好的路徑沒被在對應規模下驗證過」,Cloudflare 則示範了三段路都補上之後,一次全球性的嚴重事故可以被壓縮到不到半小時。第十六節「用規則測試描述你期待的行為」與第八至九節的 SRE Lab 步驟,分別對應修補中間兩段路;第一段路——把訊號真的變成 alert——是第八節 contract 的存在理由;而 Cloudflare 案例補的,是這整條地圖走完之後應該長什麼樣子。
Etsy、Honeycomb、Fastly、Knight Capital、AWS S3、Cloudflare 這六個案例合起來說明同一件事的六個面向。Etsy 的 Opsweekly 是在事前就把「這則 alert 有沒有用」變成每週被檢視的數字;Honeycomb 的 brown-out 事件顯示,就算訊號被正確設計成 alert,如果量測方法本身結構性地看不到某一類故障(二元式健康檢查看不到間歇性、部分性的退化),alert 照樣不會響;Knight Capital 顯示訊號如果沒被設計成 alert,再多封 email 也等於沒發生過;Fastly 的案例顯示,就算偵測夠快,事故現場如果沒有能立刻縮短「找根因」時間的既有路徑,總恢復時間還是會被那段摸索拖長;AWS S3 則再往後補一刀:就算找到根因、知道該做什麼,那個「該做什麼」如果從未在真實規模下被驗證過,執行本身也會變成新的瓶頸;Cloudflare 則示範了前面幾段路都補上、加上一個事先核准的緊急動作之後,一次全球性的嚴重事故可以被壓縮到 27 分鐘。
把這六個案例排成一條軸線,其實就是一份 alert 系統從「訊號存在」走到「使用者恢復正常」要經過的完整旅程:訊號要先被正確量測(Honeycomb 的反例),量測到之後要被設計成真正的 alert(Knight Capital 的反例),alert 響了之後要能快速鎖定方向(Fastly 的反例),鎖定方向之後要有驗證過的動作可以執行(AWS S3 的反例),而且這個動作最好是事先就核准好、不需要臨場討論授權(Cloudflare 的正面案例),最後還要有一個持續的機制去問「這整套流程上一次真的有用嗎」(Etsy 的 Opsweekly)。好的 alert 設計不是一次性寫完就結束,而是要有機制持續確認:訊號有沒有被正確量測、有沒有被當成 alert、方向有沒有被快速鎖定、動作有沒有被真的驗證過且事先核准。
如果這是真實 production system,我會做四件事:把 availability、latency 這類直接對應使用者體驗的訊號接上 error-budget burn alert;替每條會 paging 的 alert 附上經過演練的 runbook,而不是寫完就放著;替最常見、後果最可控的那幾種故障模式,事先核准一到兩個像 Cloudflare 全域 kill switch 那樣「不用臨場請示就能執行」的緊急動作,把決策摩擦從事故現場搬到平常的變更審查會議;再定期用類似 Opsweekly 的方式,讓值班工程師標記每則 alert 是否 actionable,用這個比例當作要不要精簡告警規則的依據,而不是等到告警多到沒人想理才回頭整理。alert 的成功不是觸發,而是讓正確的人在最短的決策摩擦下,及時採取正確動作。
一條 alert rule 不只是 expr。
它其實是一份很小、但在半夜會突然變得很重要的服務契約。
rule 裡寫的 expression 只回答「什麼時候亮」。
真正的 contract 還要回答:誰收到、收到的是哪一種影響、他可以先做什麼、何時應該停止做什麼。
如果這些答案散在 Grafana panel、Slack 訊息、某個人的腦袋與三個月前的 ticket 裡,等同沒有 contract。
先把它寫成一張卡。
Alert name: AskErrorBudgetFastBurn
User journey: POST /ask
User impact: 可回答的請求正在快速消耗可用 error budget
Primary SLI: good / total completed requests
Objective: 99.5% in 30 days
Window pair: 5m + 1h
Owner: ai-platform-oncall
Severity: page
First safe action: 依既定變更流程切換已核准的 degraded route
Do not do: 不為了止血而無限制 retry 或任意關閉 safety policy
Evidence links: service dashboard、trace search、deployment annotation
Runbook: runbooks/ask-error-budget-fast-burn.md
Escalate when: 既定 degradation 仍無法降低 burn 或安全狀態不明
這張卡不是另一份文件負擔。
它是先把「這條規則值得存在嗎」問清楚。
下面這張表可以直接放進 team 的 alert review template。
| 欄位 | 需要寫清楚的內容 | 常見失敗寫法 |
|---|---|---|
| 名稱 | 使用者旅程與症狀 | HighCPU |
| SLI | 分子、分母、排除條件 | error rate |
| 目標 | SLO、window、error budget | 要穩定 |
| 受眾 | 哪個 on-call rotation | dev team |
| 嚴重度 | page、ticket 或 dashboard | critical |
| 第一動作 | 已核准且可逆的行動 | investigate |
| 證據 | 要開哪張圖、哪個 trace filter | 看 Grafana |
| 抑制 | 哪一個症狀 alert 出現時不要重複叫人 | 避免重複 |
| 解除 | 條件恢復後如何觀察 | 恢復就好 |
| 限制 | 抽樣、延遲、缺少的資料 | 不寫 |
HighCPU 不是不能存在。
它比較像 diagnostic signal。
當 CPU 高但使用者 SLI 沒受影響,on-call 未必需要立刻被叫醒。
反過來說,AskErrorBudgetFastBurn 即使暫時還不知道是 CPU、provider 還是某個 deploy 造成,也已經知道使用者旅程正在受傷。
這是 symptom alert 優先於 cause alert 的理由。
傳統 API 發生錯誤時,常先比 code deploy。
AI workflow 還可能因為 prompt、retrieval index、model route 或 tool policy 改變而劣化。
這些不是高基數 metric label 的候選人。
它們是 investigation context。
因此把它們寫進 contract 的證據欄,而不是塞進每一筆 counter:
deployment_id: api-2026-09-25.3
workflow_version: policy-qa-v4
prompt_version: answer-with-citations-v12
retrieval_index_version: handbook-2026-09-20
model_route: primary-a
alert 觸發後,runbook 再用這些值去查 deployment annotation、trace 與 log。
Prometheus 留下可聚合的低基數維度就夠了。
例如 service、environment、route、workflow_name、outcome。
不要把 request_id、完整 prompt、使用者 ID 或文件內容放進 label。
那會讓 time series 數量跟請求一起爆炸,也會把不該出現在 metrics 的資料擴散出去。
一條 rule 若要進 pager,至少應符合下列全部條件:
for window 已被解釋,不是憑直覺填 5m。最後一點特別重要。
「rule 可以載入」只代表 YAML 沒壞。
它不代表 page 真的能協助恢復。
寫過幾次 alert contract 之後,很容易掉進另一個陷阱:把「欄位數量」當成「完整度」的代理指標,於是拚命往表格裡加東西——加更多 dashboard 連結、加更多可能原因、加更多 escalation 路徑。結果是一份很長、卻沒有更好用的文件。
真正決定 contract 好不好用的,不是欄位多寡,是每個欄位回答的問題夠不夠具體。「Evidence links」欄位放十個 dashboard 連結,不如放兩個、但註明「先看這個,再看那個,看到什麼就代表什麼」。第九欄「限制」也是同樣道理,一句「資料可能不完整」等於沒寫,「抽樣率 10%,週末流量低時信心區間會變寬」才是有用的限制說明。
這裡也該補上一個第八節原始表格沒有明說、但第五節 Cloudflare 案例已經示範過的欄位:First safe action 的執行權限,要不要先問過誰。多數 contract 只寫了「第一個安全動作是什麼」,卻沒寫「這個動作現在就能執行,還是要先徵得誰同意」。把這個授權層級寫進 contract,會長得像這樣:
First safe action: 依既定變更流程切換已核准的 degraded route
Authority: on-call 工程師可直接執行,無需額外核准
Escalate when: 既定 degradation 仍無法降低 burn 或安全狀態不明
如果一個「安全動作」寫在 runbook 裡,卻沒有明確授權層級,事故現場的 on-call 工程師大概率會多花幾分鐘去確認「我真的可以做這件事嗎」——這幾分鐘看起來不多,但在真正的高壓事故裡,往往就是拖慢恢復速度的那個環節。
把所有異常都丟到一個 severity 欄位,久了只會得到 warning 和 critical 的語意混亂。
比較實際的做法,是先把訊號送到不同處理節奏。
metric / event
|
+-- 是否正在傷害使用者 SLI 或快速消耗 error budget?
| |
| +-- 是 -> 是否有立即且安全的 owner action?
| | |
| | +-- 是 -> page
| | +-- 否 -> incident / escalation queue
| |
| +-- 否 -> 是否需要在工作日處理?
| |
| +-- 是 -> ticket / review queue
| +-- 否 -> dashboard / trend review
|
+-- 是否屬於稽核或安全事件?
|
+-- 走安全事件流程,不假裝成一般 SRE page
這不是讓所有問題都變得不重要。
而是讓每一種問題有正確的 time-to-respond。
| 去處 | 適合的訊號 | 回應時間的意義 | 範例 |
|---|---|---|---|
| Pager | 正在傷害或即將快速傷害使用者 SLO | 分鐘 | /ask fast burn |
| Incident queue | 已知影響,但第一線沒有安全修復權限 | 分鐘到小時 | 未核准的模型路由被停用 |
| Ticket / review queue | 穩定但需要排程改善 | 工作日 | evaluator 在抽樣上持續下降 |
| Dashboard | 需要脈絡、尚無行動門檻 | 例行檢視 | 每個 tool 的平均耗時 |
一筆 evaluator 判定「可能未 grounded」通常進 review queue。
連續多個 evaluation window 顯示大範圍品質惡化,且已證實使用者任務成功率受影響,才可能升級成 incident。
這裡的差別不是 AI 問題比較不嚴重。
差別在於單筆 evaluator 可能有誤判、可能只覆蓋抽樣,也常沒有一個凌晨兩點能安全執行的修復動作。
一個實用的命名方式是:
severity=page
severity=ticket
severity=info
再用其他 label 描述問題:
signal_type=sli_burn
signal_type=dependency
signal_type=quality_review
signal_type=capacity
這樣 severity=page 的意思很單純:現在要有人接手。
signal_type 則讓 dashboard、routing 與週報知道這是什麼類型的訊號。
不要把 severity=critical 同時拿來表示「這是資安問題」、「這是 GPU 問題」與「這個人很焦慮」。
第一個是單一 dependency error。
某次 LLM provider 失敗,可能已由 fallback 成功處理,也可能只影響到一筆請求。
先看 end-to-end outcome。
第二個是單筆 hallucination。
它值得保存、標註、回收成 regression case。
但它不是自動等於整個服務正在 outage。
第三個是資源使用率尖峰。
GPU utilization、CPU、queue depth 都很有價值。
不過它們需要和排隊延遲、拒絕率或使用者 SLI 一起判讀。
原因 alert 可以在 dashboard 上亮。
page 則應優先代表症狀。
安全事件是常見例外。
例如偵測到憑證外洩、未授權資料存取或明確的 policy breach,處理路徑可能必須立刻啟動。
它不一定等到 availability SLO 壞掉。
但這應該接到 security incident policy、指定 owner 與保全證據的流程。
不要只是把它硬塞進 AskHighLatency 的 pager。
有個常見誤解,是把「分流表」設計成跟公司組織圖一一對應——每個 team 一條 routing 規則、每個訊號類型對應一個部門。這種做法短期看起來很合理,長期會出兩個問題:第一,組織架構改組的頻率往往比故障模式的分類邏輯更高,routing 表因此變得比實際需要更脆弱;第二,一個真實事故常常同時牽涉多個 team 的訊號(例如 /ask 的 bad outcome 同時牽涉 retrieval、model route、tool policy),如果分流表按組織圖切,反而會把本來屬於同一個 incident 的訊號拆給不同的人各自處理,沒人看到完整全貌。
比較穩健的做法,是先按「使用者旅程」分流,再由 incident commander 或既定的 escalation policy 決定要不要拉進其他 team 的人,而不是讓 routing 表本身承擔「這個問題該找誰」的全部判斷責任。這也是為什麼第八節的 contract 要求每條 page rule 都寫清楚 owner——owner 負責的是「這個使用者旅程」,不是「這個技術元件」,這個分別在多依賴、多 team 協作的 AI workflow 裡格外重要。
上面的分流圖裡,「已知影響,但沒有安全修復權限」被單獨拉出一條路徑,而不是直接塞進 page 或 ticket 二選一。這個中間狀態很容易被忽略,卻是實務上經常出現的情境:某個訊號確實已經威脅到使用者 SLO,但第一線 on-call 沒有權限執行修復(例如需要動用一個尚未核准給 on-call 的高風險動作),這時候硬塞進 page 只會讓收到通知的人乾瞪眼,塞進 ticket 又低估了它的急迫性。incident queue 存在的意義,是承認「有些問題確實急,但急迫不等於第一線有能力處理」,讓這類訊號走向明確的升級路徑,而不是被迫在兩個都不合適的選項裡硬選一個。
以下範例是假設你已有一個 /ask service。
它不要求讀者照抄 metric 名稱。
真正要複製的是:先定義成功,再用低基數 label 把成功與失敗量出來。
ai_requests_total
type: counter
labels: service, environment, route, workflow_name, outcome
outcome: success | technical_failure | workflow_failure | rejected
ai_workflow_duration_seconds
type: histogram
labels: service, environment, workflow_name, route
measures: ingress 到最後回應的端到端耗時
dependency_calls_total
type: counter
labels: service, environment, dependency, operation, outcome
outcome: success | timeout | error
這裡故意沒有 model_name、prompt_version、request_id。
如果 model route 的數量固定而且很少,團隊可以評估加進 label。
但一旦 route 會被實驗或租戶動態產生,它就不適合當 metrics 維度。
把動態版本資訊留在 trace、structured log 或 deployment annotation。
直接在每條 alert 裡塞一整串分子分母,最終一定會有兩條 rule 算出不同的「失敗」。
先把語意固定成 recording rule。
以下 YAML 是讀者可放進自己的 Prometheus rules 檔案的範本。
請先把 metric 名稱、job label 與 SLO 改成你的實際 contract。
groups:
- name: ai-service-recording-rules
interval: 30s
rules:
- record: job:ask_completed_requests:rate5m
expr: |
sum by (service, environment, route, workflow_name) (
rate(ai_requests_total{route="/ask"}[5m])
)
- record: job:ask_good_requests:rate5m
expr: |
sum by (service, environment, route, workflow_name) (
rate(ai_requests_total{
route="/ask",
outcome="success"
}[5m])
)
- record: job:ask_bad_outcome_ratio:5m
expr: |
1 - (
job:ask_good_requests:rate5m
/
clamp_min(job:ask_completed_requests:rate5m, 1)
)
- record: job:ask_error_budget_burn:5m
expr: |
job:ask_bad_outcome_ratio:5m / (1 - 0.995)
0.995 是範例 SLO 的 good-event objective。
它不是所有服務都該採用的數字。
clamp_min 是為了避免沒有流量時除以零。
不過這不表示低流量服務可以忽略 SLO 設計。
對低流量旅程,event-based SLI、較長 window 或合成測試可能比較適合;這要回到 Day 17 的 SLI contract,而不是用一條 query 假裝解決。
- record: job:ask_completed_requests:rate1h
expr: |
sum by (service, environment, route, workflow_name) (
rate(ai_requests_total{route="/ask"}[1h])
)
- record: job:ask_good_requests:rate1h
expr: |
sum by (service, environment, route, workflow_name) (
rate(ai_requests_total{
route="/ask",
outcome="success"
}[1h])
)
- record: job:ask_bad_outcome_ratio:1h
expr: |
1 - (
job:ask_good_requests:rate1h
/
clamp_min(job:ask_completed_requests:rate1h, 1)
)
- record: job:ask_error_budget_burn:1h
expr: |
job:ask_bad_outcome_ratio:1h / (1 - 0.995)
短 window 反應快。
長 window 讓你不會因為一個短暫尖峰就叫醒人。
兩個 window 同時成立時,才比較像一個持續中的使用者影響。
Google SRE Workbook 的 multiwindow、multi-burn-rate 方式,是用不同長短 window 平衡偵測速度與誤報。
下列 14.4 不是神奇常數。
它是常見的快燃範例門檻;是否適合,要依你的 objective、合約與能接受的 error-budget 消耗比例校正。
- alert: AskErrorBudgetFastBurn
expr: |
(
job:ask_error_budget_burn:5m > 14.4
)
and
(
job:ask_error_budget_burn:1h > 14.4
)
for: 2m
keep_firing_for: 5m
labels:
severity: page
service: policy-qa
signal_type: sli_burn
owner: ai-platform-oncall
annotations:
summary: "/ask 正在快速消耗 error budget"
description: >-
/ask 的 5m 與 1h error-budget burn 都超過 fast-burn threshold。
先確認使用者 outcome、最近變更與 dependency 狀態;再依 runbook
執行已核准的 degradation。
runbook_url: "https://example.invalid/runbooks/ask-error-budget-fast-burn"
dashboard_url: "https://example.invalid/grafana/d/ask-overview"
Prometheus alerting rule 的 expr 決定條件。
for 表示條件持續多久後才從 pending 進入 firing。
keep_firing_for 則讓條件剛恢復時仍保留一段 firing 時間,減少通知反覆開關。
這些是目前 Prometheus rule schema 支援的欄位;但你仍要確認自己的 Prometheus 版本與部署流程。
14.4 這個數字是怎麼來的,不要照抄卻不知道為什麼多窗口燒錢速率(multiwindow burn rate)的核心想法,其實是一道很直觀的算式:如果你在 30 天的 window 裡只被允許花掉 0.5% 的 error budget(objective 是 99.5%),而你希望「如果照現在的速度燒下去,一小時內就會把整個月的預算燒光」這種急迫程度才觸發快燃告警,那麼可以反推出目前的燒錢速度必須是正常速度的多少倍。
30 天 = 720 小時
一小時燒光整月預算,代表燒錢速度是「正常分攤速度」的 720 倍
但這樣的門檻太敏感,一個小尖峰就會 firing
於是 Google SRE Workbook 取一個折衷:
1 小時燒掉 2% 預算就示警(而不是 100%)
720 * 2% = 14.4
14.4 不是一個隨手挑的吉祥數字,是「一小時內燒掉整月預算的 2%」反推出來的倍率,對應的是「6 小時內會把整月預算燒光」這種急迫程度。如果你把 SLO 目標從 99.5% 改成 99.9%,或是把 window 從 30 天改成 7 天,這個倍率就要重新算,不能原封不動照抄。這正是本節開頭強調「不是神奇常數」的原因——它是從你自己的 SLO 目標與可接受的預算消耗速度反推出來的結果,換了前提,數字就要跟著換。
rate() 的 window 跟 alert 的 for window,是兩件不同的事,很多人搞混一個常見的誤解,是把 rate(...[5m]) 裡的 5m 跟 alert 規則裡的 for: 2m 當成同一種「時間窗」在理解,兩者其實回答完全不同的問題。rate(...[5m]) 決定的是「這個數值本身是用過去多長時間的資料算出來的平均速率」——它影響的是數值的平滑程度,窗口越短,數值對瞬間波動越敏感;窗口越長,數值越平滑,但反應越慢。for: 2m 決定的是「這個條件要連續成立多久,才從 pending 狀態轉成真正 firing」——它是一道額外的持續性檢查,用來過濾掉曇花一現的尖峰。
兩者疊加起來,才是完整的行為:rate(...[5m]) 算出的燒錢速率如果超過門檻,rule 先進入 pending 狀態;這個 pending 狀態要持續滿 for 指定的時間,才真正 firing、送出通知。如果你只調 for 卻沒意識到 rate() 的窗口早就把短暫尖峰平滑掉了,或者反過來,改了 rate() 窗口卻沒重新想過 for 該設多久,得出的行為往往跟直覺預期不一樣。
ai_requests_total 這類 counter 有一個 Prometheus 使用者遲早會踩到的特性:服務重啟、container 被 OOM killed、或是任何讓程式重新啟動的情況,都會讓 counter 從零重新累積。rate() 函式內建了偵測 counter reset 的邏輯,會自動忽略看起來「往回跳」的數值,避免算出負的速率;但如果重啟頻繁到窗口內發生多次 reset,短窗口的 rate() 讀數還是可能失真幾個 evaluation cycle。這也是為什麼第十六節的驗收清單要求「至少推演正常、尖峰、持續故障與 fallback 成功四種情境」——如果你的服務有明顯的滾動重啟(rolling restart)習慣,值得多加一種「重啟期間」的情境去推演,確認不會因為 counter reset 誤觸發。
讀者常會先量 LLM call latency。
它對排查很有用。
但是使用者等待的是整條 workflow:ingress、retrieval、模型、tool、parser 與 response。
以下範例以端到端 histogram 為 alert 依據。
- record: job:ask_latency_p95_seconds:5m
expr: |
histogram_quantile(
0.95,
sum by (le, service, environment, route, workflow_name) (
rate(ai_workflow_duration_seconds_bucket{route="/ask"}[5m])
)
)
- alert: AskLatencySLOBreachRisk
expr: |
job:ask_latency_p95_seconds:5m > 4
and
job:ask_latency_p95_seconds:1h > 4
for: 5m
labels:
severity: page
service: policy-qa
signal_type: latency
owner: ai-platform-oncall
annotations:
summary: "/ask P95 端到端延遲持續高於 4 秒"
description: >-
先確認此門檻是否對應已定義的使用者 latency SLO,
再從 workflow breakdown 區分 queue、retrieval、model 或 tool。
runbook_url: "https://example.invalid/runbooks/ask-latency"
範例中的 4 秒只是一個假值。
它只能在「產品真的把 P95 小於四秒當作承諾」時才有意義。
不要因為 dashboard 畫得出 P95,就把它自動變成 pager 門檻。
provider timeout 很可能是根因。
但根因 alert 不該在使用者沒受影響時搶先 page。
下列 rule 的要點不是 0.2。
而是 dependency error 和 /ask 的 bad outcome 必須同時成立。
- record: job:llm_timeout_ratio:5m
expr: |
sum by (service, environment) (
rate(dependency_calls_total{
dependency="llm_provider",
outcome="timeout"
}[5m])
)
/
clamp_min(
sum by (service, environment) (
rate(dependency_calls_total{
dependency="llm_provider"
}[5m])
),
1
)
- alert: AskProviderTimeoutContributing
expr: |
job:llm_timeout_ratio:5m > 0.20
and on (service, environment)
job:ask_bad_outcome_ratio:5m > 0.02
for: 5m
labels:
severity: ticket
service: policy-qa
signal_type: dependency
owner: ai-platform
annotations:
summary: "LLM provider timeout 正在伴隨 /ask outcome 劣化"
description: >-
先確認 fallback 是否成功、上游狀態與最近 model-route 變更。
若同時有 AskErrorBudgetFastBurn,讓 symptom alert 保持主要通知。
runbook_url: "https://example.invalid/runbooks/llm-provider-timeout"
如果 AskErrorBudgetFastBurn 已經 firing,這條 dependency rule 多半不需要再製造一個 page。
它可以作為同一 incident 的排查線索。
下面會用 inhibition 把這個意圖寫進 routing。
and on (service, environment) 不是裝飾。
它要求左右兩邊以指定 label 對齊。
如果你的 recording rule 一邊還保留 workflow_name,另一邊沒有,join 可能不會如預期工作。
因此先定義 recording rule 的 output labels,通常比在 alert expression 裡臨時補 join 更安全。
閱讀 rule 時,也要問:
sum by 之後還剩哪些 label?or 是否把沒有流量誤當成成功?這些問題比「query 看起來很酷」重要得多。
contract 寫好、PromQL 也算對了,alert 才走完一半。下篇接著談:這條 rule fire 之後怎麼路由到對的人、runbook 要長什麼樣子才真的能用、以及怎麼把「這條 alert 是不是廢話」變成一件可以持續審查的事。
這篇是 Learning SRE for the AI Era 系列的一部分。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.